#!/usr/bin/python
# -*- encoding:utf-8 -*-

# ----------------------------------------------------------------------------------------------
import os
import sys
from shutil import rmtree
import pysvn
from tempfile import mkdtemp
from cStringIO import StringIO
import difflib
import tarfile,zipfile
import web
from encodings.aliases import aliases
from urlparse import urlparse
import urllib,urllib2
from UserList import UserList
from UserDict import UserDict
import time

# ----------------------------------------------------------------------------------------------
# settings
# ----------------------------------------------------------------------------------------------
class Setting(object):
    """some settings
    """
    # global settings
    # repository url
    # remember clear the cache directory after you change this reposurl
    reposurl = ''
    reposurl_no_proxy = True
    svn_username = ''
    svn_passwd = ''
    # filename encoding for files in tar
    filename_encoding = 'utf_8'
    # file content encoding for displayed in HTML
    file_encoding = 'shift_jis'
    # for valid encodings, see http://docs.python.org/library/codecs.html#standard-encodings
    fromdesc = '修正前'
    todesc = '修正後'
    # these file will not be diffed
    ignore_diff_file_ext = ['.xls','.zip','.rar','.jar','.class']
    # do not set it to True 
    quirk = True
    file_cache_dir = "cache"
    def __init__(self, **kwargs):
        for key,value in kwargs.items():
            setattr(self, key, value)

# ----------------------------------------------------------------------------------------------
class HtmlDiff(difflib.HtmlDiff):
    def make_file(self,fromlines,tolines,fromdesc='',todesc='',context=False,
                  numlines=5, encoding='shift-jis', indexpath='index.html', comment=""):
        """Returns HTML file of side by side comparison with change highlights
        
        Arguments:
        fromlines -- list of "from" lines
        tolines -- list of "to" lines
        fromdesc -- "from" file column header string
        todesc -- "to" file column header string
        context -- set to True for contextual differences (defaults to False
            which shows full differences).
        numlines -- number of context lines.  When context is set True,
            controls number of lines displayed before and after the change.
            When context is False, controls the number of lines to place
            the "next" link anchors before the next change (so click of
            "next" link jumps to just before the change).
        """
        return self._file_template % dict(
            encoding = encoding,
            indexpath = indexpath,
            comment = comment,
            styles = self._styles,
            legend = self._legend,
            table = self.make_table(fromlines,tolines,fromdesc,todesc,
                                    context=context,numlines=numlines))
    
    _file_template = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html>

<head>
    <meta http-equiv="Content-Type"
        content="text/html; charset=%(encoding)s" />
    <title></title>
    <style type="text/css">%(styles)s
    </style>
</head>
<body style="background-image:url(about:blank);background-attachment:fixed;">
<div style="position:fixed; top:0px; left:0px; height:30px; width:100%%; background-color:#FFF;
    _position:absolute;
    *top:expression(eval(document.compatMode &&
                         document.compatMode=='CSS1Compat') ?
                    documentElement.scrollTop
                    : document.body.scrollTop ); "><a href="index.html">back to index</a>
<span style="margin-left: 50px">%(comment)s</span></div>
<div style=" height:30px; width:100%%"></div>
    %(table)s%(legend)s
</body>

</html>"""
# ----------------------------------------------------------------------------------------------
class Compresser():
    def __init__(self, file, name):
        raise Exception('abstract class')
class gzCompressor():
    @staticmethod
    def getFilenameExt():
        return "tgz"
    def __init__(self, file, name):
        self.tar = tarfile.open(name=name, mode='w:gz', fileobj=file)
    def add(self, info, str):
        self.tar.addfile(info, StringIO(str))
    def close(self):
        self.tar.close()
    def make_info(self, fn, size, mtime):
        """make a TarInfo object
        """
        ti = tarfile.TarInfo()
        ti.name = fn
        ti.size = size
        ti.mtime = mtime
        ti.mode = 0644
        ti.type = tarfile.REGTYPE
        return ti

class zipCompressor():
    @staticmethod
    def getFilenameExt():
        return "zip"
    def __init__(self, file, name):
        self.zip = zipfile.ZipFile(file=file, mode='w')
    def add(self, info, str):
        self.zip.writestr(info, str)
    def close(self):
        self.zip.close()
    def make_info(self, fn, size, mtime):
        """make a ZipInfo object
        """
        zi = zipfile.ZipInfo()
        zi.filename = fn
        zi.file_size = size
        zi.date_time = time.gmtime(mtime)
        return zi
# ----------------------------------------------------------------------------------------------
class SvnDiffer(object):
    def __init__(self, setting, compressor):
        self.setting = setting
        self.compressor = compressor
        self.cache = {}
    def gen_svn_diff(self, revisions):
        """for revision numbers in revisions, generate tgz file containing diff htmls
        """
        reposurl = self.setting.reposurl.strip("/")
        reposurl_no_proxy = self.setting.reposurl_no_proxy
        
        if reposurl_no_proxy:
            os.environ['no_proxy'] = urlparse(reposurl).netloc
        client = pysvn.Client()
        if urlparse(reposurl).scheme == 'https':
            client.callback_ssl_server_trust_prompt = lambda x:(True, 0, True)
        client.callback_get_login = lambda x,y,z:(True, self.setting.svn_username, self.setting.svn_passwd, True)
        
        root_url = client.root_url_from_path(reposurl)
        revisions.sort()
        logs = client.log(root_url,
                   pysvn.Revision(pysvn.opt_revision_kind.number, revisions[0]),
                   pysvn.Revision(pysvn.opt_revision_kind.number, revisions[-1]),
                   discover_changed_paths = True)
        
        # all changes in revisions
        changeset = {}
        # calculate *real* action for files affected by committing of revisions.
        # the *real* action depends on the repository's statuses of earliest and latest revisions.
        # changeset is a dict which key is resource path, value is a dict,
        #     which key is revision number commited, value is the action performed in that commit
        for log in logs:
            if not log.revision.number in revisions:
                continue
            for change in log.changed_paths:
                if not change.path in changeset:
                    chg = UserDict()
                    # *real* action
                    chg.action = change.action
                    chg.first_revision = log.revision.number
                    chg.last_revision = log.revision.number
                    chg[log.revision.number] = change.action
                    changeset[change.path] = chg
                else:
                    chg = changeset[change.path]
                    chg.last_revision = log.revision.number
                    chg[log.revision.number] = change.action
                    if chg.action == "A" and change.action == "D":
                        del changeset[change.path]
                    elif chg.action == "D" and change.action == "A":
                        chg.action = "M"
                    elif chg.action == "M" and change.action == "D":
                        chg.action = "D"
        # items in index page
        # items has type(one of "Binary","DIR","File") and *real* action and revision number of committing affected
        lindex = []
        fzip = StringIO()
        zip = self.compressor(file=fzip, name='%s.'%'-'.join(map(str,revisions)) + self.compressor.getFilenameExt())
        for path, chg in changeset.items():
            bn = os.path.basename(path)
            url = root_url+urllib.quote(path)
            chg.fullname = path
            # if file is binary file defined by ignore_diff_file_ext, we do not give diff, but just file path
            if reduce(lambda x,y: x or y, (bn.endswith(ext) for ext in self.setting.ignore_diff_file_ext)):
                chg.type = "Binary"
                chg.filename = bn
                lindex.append(chg)
                continue
            # if is a directory, we can not give diff, so just file path
            if chg.action in "AD":
                if self.setting.quirk:
                    if self.quirk_info(url,
                                  revision=pysvn.Revision(pysvn.opt_revision_kind.number, chg.first_revision),
                                  recurse=False)[0][1].kind == pysvn.node_kind.dir:
                        chg.type = "DIR"
                        chg.filename = path
                        lindex.append(chg)
                        continue
                else:
                    if client.info2(url,
                                    revision=pysvn.Revision(pysvn.opt_revision_kind.number, chg.first_revision),
                                    recurse=False)[0][1].kind == pysvn.node_kind.dir:
                        chg.type = "DIR"
                        chg.filename = path
                        lindex.append(chg)
                        continue
            
            # for regular file
            # *real* action and diff
            txtold,txtnew,html = self.get_diff(root_url, path, chg.action, chg.first_revision-1, chg.last_revision)
            # old file
            # FIXME: should get the date of last commit of this file
            oti = zip.make_info('%s.old'%bn.decode('utf8').encode(self.setting.filename_encoding), len(txtold), time.time())
            zip.add(oti, txtold)
            # new file
            nti = zip.make_info('%s.new'%bn.decode('utf8').encode(self.setting.filename_encoding), len(txtnew), time.time())
            zip.add(nti, txtnew)
            # diff html
            hti = zip.make_info('%s.html'%bn.decode('utf8').encode(self.setting.filename_encoding), len(html), time.time())
            zip.add(hti, html)
            
            for revision in revisions:
                action = chg.get(revision, None)
                if action:
                    txtold,txtnew,html = self.get_diff(root_url, path, action, revision-1, revision)
                    hti = zip.make_info('%s.%d.html'%(bn.decode('utf8').encode(self.setting.filename_encoding),revision), len(html), time.time())
                    zip.add(hti, html)
            chg.type = "File"
            chg.filename = bn
            lindex.append(chg)
        
        txtindex = self.format_index_page(lindex, revisions)
        iti = zip.make_info('index.html', len(txtindex), time.time())
        zip.add(iti, txtindex)
        
        zip.close()
        
        return fzip.getvalue()
    def format_index_page(self, lindex, revisions):
        """format index page according iteratable lindex
        items in lindex has attribute of:
            type
            action
            filename
        items in lindex also is a dict which
             key is revision number commited,
             value is the action performed in that commit
        
        revisions is sorted list of revision numbers
        """
        lfiles = list()
        lfiles.append("<table border=1>")
        line = '<tr><td>Changeset in :%s</td>' % ','.join(str(revision) for revision in revisions)
        for revision in revisions:
            line += '<td>%d</td>'%revision
        line += "<td>fullpath</td></tr>"
        lfiles.append(line)
        for i in lindex:
            if i.type == "File":
                line = '<tr><td> %s %s: <a href="%s.html">%s</a></td>' % \
                 (i.action,
                   i.type,
                   i.filename.decode('utf8').encode(self.setting.file_encoding),
                   i.filename.decode('utf8').encode(self.setting.file_encoding))
                for revision in revisions:
                    action = i.get(revision, "")
                    if action:
                        line += '<td align="center"><a href="%s.%d.html">%s</a></td>' % \
                         (i.filename.decode('utf8').encode(self.setting.file_encoding),
                           revision,
                           action)
                    else:
                        line += '<td>&nbsp;</td>'
            else:
                line = "<tr><td> %s %s: %s" % (i.action, i.type, i.filename)
                for revision in revisions:
                    action = i.get(revision, "")
                    if action:
                        line += '<td align="center">%s</td>' % action
                    else:
                        line += '<td>&nbsp;</td>'
            
            line += '<td align="center"><a href="%s" title="%s">...</a></td></tr>' % tuple([i.fullname.decode('utf8').encode(self.setting.file_encoding)]*2)
            lfiles.append(line)
        lfiles.append("</table>")
        txtindex = '\n'.join(lfiles)
        return txtindex
    def get_diff(self, root_url, path, action, old_revision, new_revision):
        """get diff of path in root_url between old_revision and new_revision
        return txtold, txtnew, html
        """
        url = root_url+urllib.quote(path)
        if action in "MD":
            if self.setting.quirk:
                txtold = self.quirk_cat(root_url,
                                   path,
                                   old_revision,
                                   self.setting.svn_username,
                                   self.setting.svn_passwd)
            else:
                txtold = client.cat(url,
                                    pysvn.Revision(pysvn.opt_revision_kind.number, old_revision))
        else:
            txtold = ""
        
        if action in "AM":
            if self.setting.quirk:
                txtnew = self.quirk_cat(root_url,
                                   path,
                                   new_revision,
                                   self.setting.svn_username,
                                   self.setting.svn_passwd)
            else:
                txtnew = client.cat(url,
                                    pysvn.Revision(pysvn.opt_revision_kind.number, new_revision))
        else:
            txtnew = ""
        fold = StringIO(txtold)
        fnew = StringIO(txtnew)
        
        htmlDfer = HtmlDiff()
        html = htmlDfer.make_file(fold,
                                  fnew,
                                  fromdesc=self.setting.fromdesc.decode('utf8').encode(self.setting.file_encoding),
                                  todesc=self.setting.todesc.decode('utf8').encode(self.setting.file_encoding),
                                  context=False,
                                  encoding=self.setting.file_encoding,
                                  indexpath='index.html',
                                  comment='Revision %d-->%d'%(old_revision, new_revision))
        return txtold, txtnew, html
    def quirk_info(self, url, revision, recurse=False):
        ud = UserList()
        ud.kind = pysvn.node_kind.none if "." in os.path.basename(url) else pysvn.node_kind.dir
        return [[None,ud]]
    def quirk_cat(self, root_url, path, revision, user, passwd, nocache=False):
        """dirty hack substitution of pysvn.Client.cat
        here we fetch file content directly from root_url,
        ignoring the PROPFIND and REPORT svn phase,
        do not call me!
        """
        import base64
        url = root_url+ "/!svn/bc/%d"%revision +path
        print "quirk_cat:",root_url+ "/!svn/bc/%d"%revision +path
        if not nocache:
            txt = self.cache.get(url, None)
            if txt:
                print "cache hit"
                return txt
        req = urllib2.Request(url)
        req.add_header("Authorization", "Basic " + base64.encodestring(":".join([user,passwd]))[:-1])
        txt = urllib2.urlopen(req).read()
        if not nocache:
            self.cache[url] = txt
        return txt

# ----------------------------------------------------------------------------------------------
class index(object):
    encodings = list(set(aliases.values()))
    encodings.sort()
    def GET(self):
        return """
<html>
<head>
<meta http-equiv="Content-Type"
        content="text/html; charset=utf-8" />
</head>
<body><form method="POST">
Revisions(comma separated):<input type="text" name="revisions" size="100"><br/>
options:<input type="checkbox" name="customize" onclick="toggle_customize();"><br/>
<div id="customizediv" style="display:none">
reposurl:<input type="text" name="reposurl" value="%s" size="50">
<input type="checkbox" name="reposurl_no_proxy" %s>passby proxy<br/>
svn_username:<input type="text" name="svn_username" value="%s"><br/>
svn_passwd:<input type="text" name="svn_passwd" value="%s"><br/>""" \
% (Setting.reposurl, "checked" if not Setting.reposurl_no_proxy else "", Setting.svn_username, Setting.svn_passwd) + \
"""filename_encoding:<select name="filename_encoding" value="%s">""" + \
"\n".join("""<option value="%s" %s>%s</option>""" % (encoding,"selected" if encoding==Setting.filename_encoding else "", encoding) for encoding in self.encodings) + \
"""</select><br/>
file_encoding:<select name="file_encoding">""" + \
"\n".join("""<option value="%s" %s>%s</option>""" % (encoding,"selected" if encoding==Setting.file_encoding else "", encoding) for encoding in self.encodings) + \
"""</select><br/>
fromdesc:<input type="text" name="fromdesc" value="%s"><br/>
todesc:<input type="text" name="todesc" value="%s"><br/>""" \
% (Setting.fromdesc, Setting.todesc) + \
"""</div>
<input type="submit">
</form>
<script type="text/javascript">
function toggle_customize(){
    obj = document.getElementById("customizediv");
    if( obj.style.display=="block") {
        obj.style.display="none";
    } else {
        obj.style.display="block";
    }
    }
</script>
</body></html>
        """
    def POST(self):
        i = web.input()
        setting = Setting()
        if hasattr(i, 'customize'):
            for fld in  ['reposurl',
                         'reposurl_no_proxy',
                         'svn_username',
                         'svn_passwd',
                         'filename_encoding',
                         'file_encoding',
                         'fromdesc',
                         'todesc']:
                if hasattr(i,fld):
                    setattr(setting, fld, getattr(i, fld).encode('utf8'))
        revisions = map(int, i.revisions.replace(' ',',').replace(';',',').strip(",").split(","))
        revisions.sort()
        
        compressor = zipCompressor
        filename = '%s.' % '-'.join(map(str, revisions)) + compressor.getFilenameExt()
        fullpath = os.path.join(Setting.file_cache_dir, filename)
        if Setting.reposurl == setting.reposurl and os.path.exists(fullpath):
            print "file cache hit", fullpath
            tgz = open(fullpath,'rb').read()
        else:
            tgz = SvnDiffer(setting, compressor).gen_svn_diff(revisions)
        # set headers
        web.webapi.header('Content-Type', 'application/gzip', True)
        web.webapi.header('Content-Disposition', 'attatchment;filename=%s' % filename, True)
        # file cache
        if Setting.reposurl == setting.reposurl and not os.path.exists(fullpath):
            open(fullpath, "wb").write(tgz)
        return tgz

# ----------------------------------------------------------------------------------------------
if __name__ == '__main__':
    if "-c" in sys.argv:
        revisions = map(int, sys.argv[2:])
        revisions.sort()
        
        compressor = gzCompressor
        tgz = SvnDiffer(setting, compressor).gen_svn_diff(revisions, Setting())
        filename = '%s.' % '-'.join(map(str, revisions)) + compressor.getFilenameExt()
        open(filename, 'wb').write(tgz)
    else:
        urls = (
            '/*', 'index'
            )
        app = web.application(urls, globals())
        
        sys.stdout = open('log.txt','ab', 1)
        sys.stderr = sys.stdout
        open('pid.txt','wb').write('%d'%os.getpid())
        
        Setting.file_cache_dir = os.path.abspath(Setting.file_cache_dir)
        if not os.path.exists(Setting.file_cache_dir):
            os.mkdir(Setting.file_cache_dir)
        app.run()
    
